﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Xml;

namespace Maple.Weixin.MP.Cryptography
{
    internal class WXBizMsgCrypt
    {
        private readonly string token;
        private readonly string appid;
        private readonly string encodingAESKey;

        public WXBizMsgCrypt(string token, string encodingAESKey, string appid) {
            this.token = token;
            this.encodingAESKey = encodingAESKey;
            this.appid = appid;
        }

        /// <summary>
        /// 检验消息的真实性，并且获取解密后的明文。
        /// </summary>
        /// <param name="signature">签名，对应URL中的msg_signature。</param>
        /// <param name="timestamp">时间戳，对应URL中的timestamp。</param>
        /// <param name="nonce">随机串，对应URL中的nonce。</param>
        /// <param name="postData">密文，对应POST的数据。</param>
        /// <param name="message">解密后的原文，当return返回0时有效。</param>
        /// <returns>解密成功返回0，否则为错误码。</returns>
        public int DecryptMessage(string signature, string timestamp, string nonce, string postData, ref string message) {
            if (encodingAESKey.Length != 43)
                return (int) WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey;

            var doc = new XmlDocument();
            string encryptMessage;
            try {
                doc.LoadXml(postData);
                XmlNode root = doc.FirstChild;
                encryptMessage = root["Encrypt"].InnerText;
            }
            catch (Exception) {
                return (int) WXBizMsgCryptErrorCode.WXBizMsgCrypt_ParseXml_Error;
            }

            // verify signature
            var ret = VerifySignature(token, timestamp, nonce, encryptMessage, signature);
            if (ret != 0)
                return ret;
            
            // decrypt
            string cpid = "";
            try {
                message = Cryptography.DecryptAES(encryptMessage, encodingAESKey, ref cpid);
            }
            catch (FormatException) {
                return (int) WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecodeBase64_Error;
            }
            catch (Exception) {
                return (int) WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecryptAES_Error;
            }
            if (cpid != appid)
                return (int) WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateAppid_Error;

            return 0;
        }

        /// <summary>
        /// 将回复用户的消息加密打包。
        /// </summary>
        /// <param name="replyMessage">回复用户的消息，XML格式的字符串。</param>
        /// <param name="timestamp">时间戳。</param>
        /// <param name="nonce">随机串，可以自己生成，也可以用URL中的nonce。</param>
        /// <param name="encryptMessage">加密后的密文。包括msg_signature, timestamp, nonce, encrypt的XML格式字符串。</param>
        /// <returns>成功返回0，否则返回错误码。</returns>
        public int EncryptMessage(string replyMessage, string timestamp, string nonce, ref string encryptMessage) {
            if (encodingAESKey.Length != 43)
                return (int) WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey;

            string raw;
            try {
                raw = Cryptography.EncryptAES(replyMessage, encodingAESKey, appid);
            }
            catch (Exception) {
                return (int) WXBizMsgCryptErrorCode.WXBizMsgCrypt_EncryptAES_Error;
            }

            string signature = "";
            int ret = GenerateSignature(token, timestamp, nonce, raw, ref signature);
            if (ret != 0)
                return ret;

            encryptMessage = "";
            const string encryptLabelHead = "<Encrypt><![CDATA[";
            const string encryptLabelTail = "]]></Encrypt>";
            const string msgSigLabelHead = "<MsgSignature><![CDATA[";
            const string msgSigLabelTail = "]]></MsgSignature>";
            const string timeStampLabelHead = "<TimeStamp><![CDATA[";
            const string timeStampLabelTail = "]]></TimeStamp>";
            const string nonceLabelHead = "<Nonce><![CDATA[";
            const string nonceLabelTail = "]]></Nonce>";
            encryptMessage = encryptMessage + "<xml>" + encryptLabelHead + raw + encryptLabelTail;
            encryptMessage = encryptMessage + msgSigLabelHead + signature + msgSigLabelTail;
            encryptMessage = encryptMessage + timeStampLabelHead + timestamp + timeStampLabelTail;
            encryptMessage = encryptMessage + nonceLabelHead + nonce + nonceLabelTail;
            encryptMessage += "</xml>";
            return 0;
        }

        private static int VerifySignature(string token, string timestamp, string nonce, string encryptMessage,
            string signature) {
            string hash = "";
            int ret = GenerateSignature(token, timestamp, nonce, encryptMessage, ref hash);
            if (ret != 0)
                return ret;
            if (hash == signature)
                return 0;
            return (int) WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateSignature_Error;
        }

        private static int GenerateSignature(string token, string timestamp, string nonce, string encryptMessage,
            ref string signature) {
            var list = new List<string> {token, timestamp, nonce, encryptMessage};
            list.Sort(new DictionarySort());
            var raw = list.Aggregate("", (current, t) => current + t);
            string hash;
            try {
                SHA1 sha = new SHA1CryptoServiceProvider();
                var encoding = new ASCIIEncoding();
                var dataToHash = encoding.GetBytes(raw);
                var dataHashed = sha.ComputeHash(dataToHash);
                hash = BitConverter.ToString(dataHashed).Replace("-", "").ToLower();
            }
            catch (Exception) {
                return (int) WXBizMsgCryptErrorCode.WXBizMsgCrypt_ComputeSignature_Error;
            }
            signature = hash;
            return 0;
        }

        class DictionarySort : IComparer<string>
        {
            public int Compare(string oLeft, string oRight)
            {
                string sLeft = oLeft ?? "";
                string sRight = oRight ?? "";
                int iLeftLength = sLeft.Length;
                int iRightLength = sRight.Length;
                int index = 0;
                while (index < iLeftLength && index < iRightLength) {
                    if (sLeft[index] < sRight[index])
                        return -1;
                    if (sLeft[index] > sRight[index])
                        return 1;
                    index++;
                }
                return iLeftLength - iRightLength;

            }
        }
    }
}
